왜 다를까 행 제한·페이징 자동증가 PK 문자열 NULL 처리 날짜·시간 자료형·식별자 프로시저·함수·트리거
◀ CH 03 📋 목차
🗄️ 부록 · 실무

DBMS별 SQL 문법 차이 & 프로시저

같은 일을 해도 MySQL · PostgreSQL · Oracle · SQL Server는 문법이 조금씩 달라요. 회사 DB가 바뀌면 헷갈리는 부분만 모았고, 마지막에 스토어드 프로시저·함수·트리거도 다룹니다.

🤔
왜 다를까
SQL은 "표준"이 있는데, 왜 DB마다 다를까?
표준(ANSI SQL)은 있지만, 회사(벤더)마다 조금씩 다르게 구현했어요.

SQL에는 ANSI/ISO 표준이 있어서 SELECT·JOIN·WHERE 같은 기본은 거의 똑같아요. 하지만 "상위 N개 뽑기", "자동 증가 번호", "문자열 합치기", "현재 시각" 같은 세부 기능은 벤더마다 다르게 만들었어요. 그래서 MySQL에서 되던 게 Oracle에선 안 되기도 해요.

🧭
실무 팁. 가능하면 표준 문법(예: COALESCE, FETCH FIRST)을 쓰면 DB가 바뀌어도 잘 돌아가요. 벤더 전용 문법(NVL·TOP 등)은 그 DB에서만 동작한다는 걸 기억하세요.
주제MySQL / MariaDBPostgreSQLOracleSQL Server
상위 N행LIMIT nLIMIT nFETCH FIRST n ROWS ONLYTOP n
NULL 대체IFNULLCOALESCENVLISNULL
문자열 연결CONCAT()||||+
현재 시각NOW()NOW()SYSDATEGETDATE()
자동 증가 PKAUTO_INCREMENTSERIALSEQUENCEIDENTITY

👇 아래에서 항목별로 자세한 예제와 함께 볼게요.

🔢
행 제한·페이징
상위 N개 뽑기 & 페이지네이션
CH03에서 배운 LIMIT — DB마다 이렇게 달라요
DBMS상위 N행페이지네이션(건너뛰기)
MySQL·PostgreSQL·SQLiteLIMIT 5LIMIT 5 OFFSET 10
SQL ServerSELECT TOP 5 ...OFFSET 10 ROWS FETCH NEXT 5 ROWS ONLY
Oracle (12c+)FETCH FIRST 5 ROWS ONLYOFFSET 10 ROWS FETCH NEXT 5 ROWS ONLY
Oracle (구버전)ROWNUM을 서브쿼리로 감싸서 사용
-- 급여 높은 상위 5명

-- MySQL / PostgreSQL / SQLite
SELECT * FROM emp ORDER BY salary DESC LIMIT 5;

-- SQL Server
SELECT TOP 5 * FROM emp ORDER BY salary DESC;

-- Oracle 12c+
SELECT * FROM emp ORDER BY salary DESC FETCH FIRST 5 ROWS ONLY;

-- Oracle 구버전 (ROWNUM은 정렬 전에 매겨지므로 서브쿼리로 감싼다)
SELECT * FROM (
  SELECT * FROM emp ORDER BY salary DESC
) WHERE ROWNUM <= 5;
⚠️
오라클 ROWNUM정렬(ORDER BY)이 적용되기 전에 번호가 매겨져요. 그래서 "정렬 후 상위 N개"는 반드시 서브쿼리로 감싼 뒤 ROWNUM을 걸어야 정확해요.
🔑
자동증가 PK
기본키를 자동으로 1씩 증가
테이블 만들 때(CH09) 벤더마다 다른 부분
-- MySQL / MariaDB
CREATE TABLE member (
  id INT PRIMARY KEY AUTO_INCREMENT,
  name VARCHAR(50)
);

-- PostgreSQL (SERIAL, 또는 표준 IDENTITY)
CREATE TABLE member (
  id SERIAL PRIMARY KEY,             -- 또는 GENERATED ALWAYS AS IDENTITY
  name VARCHAR(50)
);

-- SQL Server
CREATE TABLE member (
  id INT IDENTITY(1,1) PRIMARY KEY,  -- 1부터 1씩 증가
  name VARCHAR(50)
);

-- Oracle 12c+ (IDENTITY) / 구버전은 SEQUENCE + 트리거
CREATE TABLE member (
  id NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
  name VARCHAR2(50)
);
DBMS자동 증가 방식
MySQLAUTO_INCREMENT
PostgreSQLSERIAL / GENERATED AS IDENTITY
SQL ServerIDENTITY(시작, 증가폭)
OracleSEQUENCE(+트리거) / 12c+ IDENTITY
🔤
문자열
연결 · 부분 추출 · 길이
특히 "문자열 합치기"가 제일 헷갈려요
-- 성 + 이름 합치기

-- MySQL (|| 는 기본적으로 'OR'로 해석 → CONCAT 사용)
SELECT CONCAT(first_name, ' ', last_name) FROM emp;

-- PostgreSQL / Oracle / SQLite  (표준 || 연결)
SELECT first_name || ' ' || last_name FROM emp;

-- SQL Server ( + 로 연결, 또는 CONCAT )
SELECT first_name + ' ' + last_name FROM emp;
SELECT CONCAT(first_name, ' ', last_name) FROM emp;   -- CONCAT은 대부분 지원
기능MySQLPostgreSQL/OracleSQL Server
연결CONCAT(a,b)a || ba + b
부분 추출SUBSTRING/SUBSTRSUBSTRSUBSTRING
길이LENGTHLENGTHLEN
💡
CONCAT()는 MySQL·PostgreSQL·SQL Server·Oracle 대부분에서 동작해요. 어느 DB에서든 안전하게 쓰고 싶으면 CONCAT() 을 쓰는 게 편해요.
🕳️
NULL 처리
값이 없을 때 기본값 채우기
COALESCE는 표준 — 어디서나 돼요
-- 전화번호가 없으면 '미등록'으로

-- 표준 (모든 DB에서 동작) ✅ 권장
SELECT COALESCE(phone, '미등록') FROM member;

-- MySQL
SELECT IFNULL(phone, '미등록') FROM member;
-- Oracle
SELECT NVL(phone, '미등록') FROM member;
-- SQL Server
SELECT ISNULL(phone, '미등록') FROM member;
💡
COALESCE(값, 기본값)ANSI 표준이라 4대 DB 전부에서 돌아가요. IFNULL·NVL·ISNULL은 각자 그 DB 전용이에요.
📅
날짜·시간
현재 시각 & 날짜 형식화
함수 이름이 벤더마다 달라요
기능MySQLPostgreSQLOracleSQL Server
현재 시각NOW()NOW()SYSDATEGETDATE()
오늘 날짜CURDATE()CURRENT_DATETRUNC(SYSDATE)CAST(GETDATE() AS DATE)
형식 문자열DATE_FORMATTO_CHARTO_CHARFORMAT/CONVERT
-- '2026-07-09' 형태로 출력
-- MySQL
SELECT DATE_FORMAT(NOW(), '%Y-%m-%d');
-- Oracle / PostgreSQL
SELECT TO_CHAR(SYSDATE, 'YYYY-MM-DD');   -- PG는 NOW() 사용
-- SQL Server
SELECT FORMAT(GETDATE(), 'yyyy-MM-dd');
🧱
자료형·식별자
타입 이름 & 이름 감싸는 따옴표
테이블 만들 때 자주 걸리는 차이
구분MySQLPostgreSQLOracleSQL Server
가변 문자열VARCHAR(n)VARCHAR(n)VARCHAR2(n)VARCHAR(n)
불리언TINYINT(1)BOOLEAN없음(NUMBER(1))BIT
대용량 텍스트TEXTTEXTCLOBVARCHAR(MAX)
이름 감싸기`col` (백틱)"col""col"[col]
🔤
문자열 은 어디서나 작은따옴표 'text'로 감싸요. 큰따옴표·백틱·대괄호는 컬럼/테이블 "이름"을 감쌀 때만 써요(그것도 벤더마다 다름).
⚙️
프로시저·함수·트리거
DB 안에 로직을 저장해 두고 부르기
스토어드 프로시저 · 함수 · 트리거
📦 스토어드 프로시저(Stored Procedure)란? 자주 쓰는 SQL 묶음(로직)을 DB 안에 이름 붙여 저장해두고, 필요할 때 이름으로 호출하는 거예요. 매번 긴 쿼리를 보내는 대신 CALL 이름(...) 한 줄로 실행하죠. 🍱 비유: 자주 먹는 메뉴를 "세트 메뉴"로 등록해두고 "1번 세트 주세요"라고 부르는 것과 같아요.
-- ▼ MySQL: 특정 부서의 인원수를 돌려주는 프로시저
DELIMITER //
CREATE PROCEDURE count_by_dept(IN dept_id INT, OUT cnt INT)
BEGIN
  SELECT COUNT(*) INTO cnt FROM emp WHERE department_id = dept_id;
END //
DELIMITER ;

-- 호출
CALL count_by_dept(10, @result);
SELECT @result;
① 파라미터 3종 — IN · OUT · INOUT
종류방향쓰임
IN입력(기본값)값을 받아서 안에서 사용 (예: 부서 번호)
OUT출력계산 결과를 밖으로 돌려줌 (예: 인원수)
INOUT입출력받은 값을 고쳐서 다시 돌려줌
② 변수 · 제어문 (조건·반복)
프로시저 안에서는 변수 선언(DECLARE), 조건(IF), 반복(WHILE·LOOP) 같은 "프로그래밍"을 할 수 있어요. 그래서 단순 쿼리를 넘어 로직을 담아요.
-- ▼ MySQL: 등급을 판정해 돌려주는 프로시저 (변수 + IF 분기)
DELIMITER //
CREATE PROCEDURE grade_of(IN score INT, OUT grade CHAR(1))
BEGIN
  IF score >= 90 THEN
    SET grade = 'A';
  ELSEIF score >= 80 THEN
    SET grade = 'B';
  ELSE
    SET grade = 'C';
  END IF;
END //
DELIMITER ;

CALL grade_of(85, @g);
SELECT @g;   -- B
-- ▼ 반복(WHILE) 예: 1..n 합계
DELIMITER //
CREATE PROCEDURE sum_to(IN n INT, OUT total INT)
BEGIN
  DECLARE i INT DEFAULT 1;
  SET total = 0;
  WHILE i <= n DO
    SET total = total + i;
    SET i = i + 1;
  END WHILE;
END //
DELIMITER ;
③ 실전 예제 — 여러 작업을 하나로 묶기
프로시저의 진짜 가치는 여러 SQL + 로직 + 트랜잭션을 한 덩어리로 묶는 거예요.
-- ▼ 주문 처리: 재고 확인 → 차감 → 주문 생성 (하나의 트랜잭션)
DELIMITER //
CREATE PROCEDURE place_order(IN p_id INT, IN qty INT, OUT ok INT)
BEGIN
  DECLARE stock INT;
  START TRANSACTION;
    SELECT quantity INTO stock FROM product WHERE id = p_id FOR UPDATE;
    IF stock >= qty THEN
      UPDATE product SET quantity = quantity - qty WHERE id = p_id;
      INSERT INTO orders(product_id, qty) VALUES (p_id, qty);
      SET ok = 1;
      COMMIT;
    ELSE
      SET ok = 0;   -- 재고 부족
      ROLLBACK;
    END IF;
END //
DELIMITER ;

호출·선언 문법은 벤더마다 조금씩 달라요.

DBMS본문 언어/문법호출
MySQLCREATE PROCEDURE ... BEGIN ... END (DELIMITER 필요)CALL 이름()
PostgreSQLCREATE PROCEDURE ... LANGUAGE plpgsqlCALL 이름()
OracleCREATE OR REPLACE PROCEDURE ... IS BEGIN ... END; (PL/SQL)EXEC 이름()
SQL ServerCREATE PROCEDURE ... AS BEGIN ... END (T-SQL)EXEC 이름()
🆚 프로시저 vs 함수 vs 트리거 · 프로시저(PROCEDURE) — 여러 작업을 수행. 값을 안 돌려주거나 OUT 파라미터로 줌. CALL/EXEC로 직접 호출.
· 함수(FUNCTION)값 하나를 반드시 반환. SELECT 안에서 SELECT fn(x)처럼 사용.
· 트리거(TRIGGER)INSERT/UPDATE/DELETE가 일어나면 자동으로 실행. 직접 호출하지 않아요(예: 이력 자동 기록). 한 줄: 프로시저=시켜서 실행, 함수=값 계산해 반환, 트리거=이벤트에 자동 반응.
-- ▼ 함수 예 (MySQL): 세금 포함 가격 반환
DELIMITER //
CREATE FUNCTION with_tax(price INT) RETURNS INT DETERMINISTIC
BEGIN
  RETURN price + (price * 0.1);
END //
DELIMITER ;

SELECT name, with_tax(price) AS total FROM product;   -- SELECT 안에서 사용

-- ▼ 트리거 예 (MySQL): 회원 삭제 시 로그 테이블에 자동 기록
CREATE TRIGGER after_member_delete
AFTER DELETE ON member
FOR EACH ROW
INSERT INTO member_log(member_id, deleted_at) VALUES (OLD.id, NOW());
④ 한 걸음 더 — 커서(Cursor)와 예외 처리
커서는 조회 결과를 한 행씩 꺼내 반복 처리할 때 써요(집합 처리로 안 될 때만 최후에). 예외 처리는 오류가 나면 잡아서 롤백·기본값 처리를 해요.
-- ▼ MySQL: 커서로 한 행씩 순회 + 예외 핸들러
DELIMITER //
CREATE PROCEDURE raise_all_salary()
BEGIN
  DECLARE done INT DEFAULT 0;
  DECLARE eid INT;
  DECLARE cur CURSOR FOR SELECT id FROM emp;              -- 커서 선언
  DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;    -- 더 없으면 종료
  DECLARE EXIT HANDLER FOR SQLEXCEPTION ROLLBACK;         -- 오류 시 롤백

  OPEN cur;
  read_loop: LOOP
    FETCH cur INTO eid;                                   -- 한 행씩 꺼냄
    IF done = 1 THEN LEAVE read_loop; END IF;
    UPDATE emp SET salary = salary * 1.1 WHERE id = eid;
  END LOOP;
  CLOSE cur;
END //
DELIMITER ;
🧭
커서는 느려요. 가능하면 UPDATE emp SET salary = salary * 1.1; 처럼 한 번에(집합) 처리하는 게 훨씬 빨라요. 커서는 "행마다 다른 복잡한 처리가 꼭 필요할 때"만 마지막 수단으로 써요.
⚠️
프로시저·트리거는 DB에 로직이 숨어 있어 편하지만, 너무 많이 쓰면 디버깅·이관이 어려워져요. 실무에선 "간단한 자동화·성능이 중요한 배치"엔 쓰고, 복잡한 비즈니스 로직은 애플리케이션(서버) 쪽에 두는 경우가 많아요.